﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Serialization;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Threading;
using System.Diagnostics;
using System.Runtime.Remoting.Messaging;

namespace sync
{
    public class Repository : INotifyPropertyChanged
    {

        private readonly object sync = new object();

        AsyncOperation async;

        private delegate void RepositoryProcessingDelegate();

        private RepositoryInfo repositoryInfo;

        private bool isReady = false;

        private bool isScaningRepository = false;

        private SendOrPostCallback onRepositoryScanningCompletedDelegate;

        public event PropertyChangedEventHandler PropertyChanged;

        public event EventHandler<AsyncCompletedEventArgs> RepositoryScanningCompleted;

        private Repository()
        {
            async = AsyncOperationManager.CreateOperation(null);
            this.InitializeDelegates();
        }

        public Repository(FileInfo repositoryInfo) : this()
        {
            RepositoryInfo repoInfoFromFile;
            Type[] infoTypes = new Type[2];
            infoTypes[0] = typeof(XmlSyncDataProviderInfo);
            infoTypes[1] = typeof(PeerConnectionProviderInfo);
            XmlSerializer xmlSerializer = new XmlSerializer(typeof(RepositoryInfo), infoTypes);

            repoInfoFromFile = xmlSerializer.Deserialize(repositoryInfo.OpenRead()) as RepositoryInfo;
            this.RepositoryInfo = repoInfoFromFile;

            this.LoadDataProvider();
            this.LoadConnectionProvider();

            this.RepositoryDirectory = new DirectoryInfo(this.RepositoryInfo.Path);
        }

        #region Properties

        public XmlSyncDataProvider DataProvider { get; private set; }

        public IConnectionProvider ConnectionProvider { get; private set; }

        public DirectoryInfo RepositoryDirectory { get; private set; }

        public List<SyncFileInfo> LocalFiles { get; private set; }

        public ObservableCollection<FileAction> FileActions { get; private set; }

        public RepositoryInfo RepositoryInfo
        {
            get
            {
                return this.repositoryInfo;
            }

            private set
            {
                if (this.repositoryInfo == value)
                    return;
                this.repositoryInfo = value;
                this.NotifyPropertyChanged("RepositoryInfo");
            }
        }

        #endregion

        public bool IsReady
        {
            get
            {
                return isReady;
            }

            set
            {
                if (isReady == value)
                    return;
                isReady = value;
                this.NotifyPropertyChanged("IsReady");
            }
        }

        public bool IsScaningRepository
        {
            get
            {
                return this.isScaningRepository;
            }
            private set
            {
                if (this.isScaningRepository == value)
                    return;
                this.isScaningRepository = value;
                this.NotifyPropertyChanged("IsScaningRepository");
            }
        }

        public void AnnounceFileList()
        {
            if (!this.CanAnnounceFileList())
                throw new Exception("");
            Trace.WriteLine(String.Format("{0} Annouce File List", this.RepositoryInfo.Id));
            this.ConnectionProvider.AnounceFileList(this.LocalFiles.Select(fi => new AnnounceFileInfo
            {
                Hash = fi.Hash,
                Name = fi.Name,
                Path = fi.Path
            }).ToList());
        }

        public void BringOnline()
        {
            this.BeginRepositoryProcessing();
            this.ConnectionProvider.BringOnline();
        }

        public static Repository Create(DirectoryInfo Path, FileInfo RepositoryInfo, XmlSyncDataProvider dataProvider, IConnectionProvider connectionProvider)
        {
            if (RepositoryInfo.Exists)
                throw new Exception();
            if (!Path.Exists)
                throw new Exception();

            RepositoryInfo repoInfo = new RepositoryInfo();
            Type[] infoTypes;

            repoInfo.Path = Path.FullName;
            repoInfo.Id = Guid.NewGuid();

            repoInfo.ClientId = Guid.NewGuid();

            dataProvider.RepositoryInfo = repoInfo;
            connectionProvider.RepositoryInfo = repoInfo;

            dataProvider.Create();
            connectionProvider.Create();

            infoTypes = (
                from cpi in Repository.GetConnectionProviderInfoTypes()
                select cpi
                )
                .Union(
                from dpi in Repository.GetDataProviderInfoTypes()
                select dpi
                ).ToArray();

            XmlSerializer xmlSerializer = new XmlSerializer(typeof(RepositoryInfo), infoTypes);

            using (FileStream fs = RepositoryInfo.Create())
            {
                xmlSerializer.Serialize(fs, repoInfo);
                fs.Close();
            }
            Repository repo = new Repository(RepositoryInfo);

            return repo;
        }

        public static Type[] GetConnectionProvidersTypes()
        {
            return Repository.GetTypesWithInterface(typeof(IConnectionProvider));
        }

        public static Type[] GetDataProviderTypes()
        {
            return Repository.GetTypesWithInterface(typeof(IDataProvider));
        }

        public static Type[] GetDataProviderInfoTypes()
        {
            return Repository.GetTypesWithInterface(typeof(IDataProviderInfo));
        }

        private static Type[] GetConnectionProviderInfoTypes()
        {
            return Repository.GetTypesWithInterface(typeof(IConnectionProviderInfo));
        }

        private static Type[] GetTypesWithInterface(Type interfaceType)
        {
            var type = typeof(IConnectionProvider);
            var types = AppDomain.CurrentDomain.GetAssemblies().ToList()
                .SelectMany(s => s.GetTypes())
                .Where(p => type.IsAssignableFrom(p));

            return types.ToArray();
        }

        private void InitializeDelegates()
        {
            this.onRepositoryScanningCompletedDelegate = new SendOrPostCallback(this.OnRepositoryScanningCompleted);
        }

        private void OnRepositoryScanningCompleted(object e)
        {
            if (this.RepositoryScanningCompleted != null)
                this.RepositoryScanningCompleted.Invoke(this, e as AsyncCompletedEventArgs);
        }
        
        private void KnowenFiles_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            //Determine File Actions and this will have to be better at determining conflicts between local and remote nodes
            IList<KnowenAnnounceFileInfo> newFileInfo = e.NewItems.Cast<KnowenAnnounceFileInfo>().ToList();

            newFileInfo = (from rf in newFileInfo
                           where rf.ClientId != this.RepositoryInfo.ClientId
                           select rf).ToList();

            var newFiles = from rf in newFileInfo
                           from lf in this.LocalFiles
                           where rf.Hash != lf.Hash
                           && rf.Path != lf.Path
                           && rf.Name != lf.Name
                           select rf;

            var movedFiles = from rf in newFileInfo
                             from lf in this.LocalFiles
                             where rf.Hash == lf.Hash
                             && (rf.Path != lf.Path || rf.Name != lf.Name)
                             select rf;

            var changedFile = from rf in newFileInfo
                              from lf in this.LocalFiles
                              where rf.Hash != lf.Hash
                              && rf.Path == lf.Path
                              && rf.Name == lf.Name
                              select rf;

            foreach (var f in newFiles)
            {
                FileCreate fileCreate = new FileCreate(this.RepositoryInfo.Path, f);
                fileCreate.RemoteStream = this.ConnectionProvider.GetStreamForFile(f);
                this.FileActions.Add(fileCreate);
            }

            foreach (var f in movedFiles)
            {
                FileMove fileMove = new FileMove(this.RepositoryInfo.Path, f);
                this.FileActions.Add(fileMove);
            }

            foreach (var f in changedFile)
            {

            }
        }

        private void ConnectionProvider_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "IsReady":
                    if (this.CanAnnounceFileList())
                        this.AnnounceFileList();
                    break;

            }
        }

        private bool CanAnnounceFileList()
        {
            return this.ConnectionProvider.IsReady && this.IsReady;
        }

        private void BeginRepositoryProcessing()
        {
            RepositoryProcessingDelegate worker = new RepositoryProcessingDelegate(this.RepositoryProcessingThread);
            AsyncCallback callback = new AsyncCallback(this.RepositoryProcessingThreadCallback);

            lock (this.sync)
            {
                if (this.isScaningRepository)
                {
                    throw new InvalidOperationException("The control is currently busy.");
                }

                worker.BeginInvoke(callback, this.async);
                this.IsScaningRepository = true;
            }

            
        }

        private void RepositoryProcessingThread()
        {
            var completeFileList = this.RepositoryDirectory.GetFiles("*", SearchOption.AllDirectories).ToList();
            List<SyncFileInfo> syncFileInfo = new List<SyncFileInfo>();
            completeFileList.ForEach(fsi =>
            {
                syncFileInfo.Add(new SyncFileInfo
                {
                    FileInfo = fsi,

                    Name = fsi.Name,
                    Path = fsi.DirectoryName.Replace(this.RepositoryInfo.Path, String.Empty),
                    ProcessingTimeStamp = DateTime.Now,
                    ModifiesDate = fsi.LastWriteTime,
                    Size = fsi.Length
                });
            });


            var filesToHash = from f in syncFileInfo
                              where f.Hash == null
                              select f;

            foreach (var f in filesToHash)
            {
                f.Hash = this.SHA1(f.FileInfo);
            }

            this.LocalFiles = syncFileInfo;

            this.IsReady = true;

            if (this.CanAnnounceFileList())
                this.AnnounceFileList();
        }

        private void RepositoryProcessingThreadCallback(IAsyncResult ar)
        {
            RepositoryProcessingDelegate worker = (RepositoryProcessingDelegate)((AsyncResult)ar).AsyncDelegate;
            AsyncOperation async = (AsyncOperation)ar.AsyncState;
            lock (this.sync)
            {
                worker.EndInvoke(ar);
                this.IsScaningRepository = false;
            }
            AsyncCompletedEventArgs args = new AsyncCompletedEventArgs(null, false, null);

            this.async.PostOperationCompleted(this.onRepositoryScanningCompletedDelegate, args);
        }

        private string SHA1(FileInfo fileinfo)
        {
            string result = String.Empty;
            string hashData = String.Empty;

            byte[] bytHashValue;
            using (FileStream fs = fileinfo.OpenRead())
            {
                System.Security.Cryptography.SHA1CryptoServiceProvider sha1Hasher =
                       new System.Security.Cryptography.SHA1CryptoServiceProvider();
                bytHashValue = sha1Hasher.ComputeHash(fs);
                fs.Close();
            }
            hashData = System.BitConverter.ToString(bytHashValue);
            hashData = hashData.Replace("-", String.Empty);
            result = hashData;

            return result;
        }

        private void NotifyPropertyChanged(string info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

        private void LoadDataProvider()
        {
            Type dataProviderType = Type.GetType(this.RepositoryInfo.LocalDataProviderType, true, false);
            Type infoType = this.RepositoryInfo.LocalDataProviderConfig.GetType();

            var constructory = dataProviderType.GetConstructor(new Type[] { infoType });
            object dataProvider = constructory.Invoke(new object[] { this.RepositoryInfo.LocalDataProviderConfig });

            this.DataProvider = dataProvider as XmlSyncDataProvider;
            this.DataProvider.RepositoryInfo = this.RepositoryInfo;

            this.DataProvider.Init();
        }

        private void LoadConnectionProvider()
        {
            Type connectionProviderType = Type.GetType(this.RepositoryInfo.ConnectionProviderType, true, false);
            Type infoType = this.RepositoryInfo.ConnectionProviderConfig.GetType();

            var constructory = connectionProviderType.GetConstructor(new Type[] { infoType });
            object dataProvider = constructory.Invoke(new object[] { this.RepositoryInfo.ConnectionProviderConfig });

            this.ConnectionProvider = dataProvider as PeerConnectionProvider;
            this.ConnectionProvider.RepositoryInfo = this.RepositoryInfo;

            this.ConnectionProvider.PropertyChanged += new PropertyChangedEventHandler(ConnectionProvider_PropertyChanged);

            this.ConnectionProvider.Init();

            this.ConnectionProvider.KnowenFiles.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(KnowenFiles_CollectionChanged);
        }

    }
}
